Skip to content

Fix login - #1160

Open
duzda wants to merge 7 commits into
freeipa:mainfrom
duzda:fix-login
Open

Fix login#1160
duzda wants to merge 7 commits into
freeipa:mainfrom
duzda:fix-login

Conversation

@duzda

@duzda duzda commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

This may be tested with the development, but it fixes a bug that exists in production. To test and replicate the fixed bug in production:

Run kinit
Navigate to Modern WebUI (You should get automatically logged in)
Log out -> Stuck in the loop.

The fix allows you to log in as another user on log out, but at the same time if you refresh page, it will still pick up kerberos. The rest is just a simplifications and few other changes, I added a link that takes you back to the old webui, I'd wish I knew who and where was asking for that, but the change seemed minor and made sense. There is also a bunch of simplifications and fixes regarding getting stuck in the login loop.

Summary by Sourcery

Prevent logout from triggering an immediate Kerberos sign-in while retaining automatic session detection and simplifying authentication state management.

New Features:

  • Add a link from the modern login page to the legacy WebUI.
  • Provide a reusable login page layout with structured instructional content.

Bug Fixes:

  • Fix logout/login loops by preventing automatic Kerberos reauthentication immediately after logout while preserving Kerberos authentication on page refresh.
  • Ensure authentication state and protected-route navigation update without requiring a full page reload.

Enhancements:

  • Centralize initial user and server metadata loading through the authentication API and global state.
  • Simplify protected route layout, logout handling, user-detail loading, and login error-state management.

Documentation:

  • Document login, session, logout, and Kerberos state-management behavior.

Tests:

  • Move router-specific test helpers into a dedicated utility module.

Chores:

  • Remove the obsolete authentication slice and redundant user-details endpoint.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/AppLayout.tsx" line_range="58-65" />
<code_context>
+  const { data: userDetails, isFetching } = useGetUserByUidQuery(loggedInUser, {
</code_context>
<issue_to_address>
**issue:** Guard against undefined `givenname` when computing `fullName` to avoid rendering `"undefined"`.

Because `userDetails?.givenname !== ""` is true when `givenname` is `undefined`, the header can show `"undefined <sn>"`. Consider a truthiness check or explicitly handling both `undefined` and empty string:

```ts
const fullName = React.useMemo(() => {
  if (!userDetails) return "";
  if (userDetails.givenname) {
    return `${userDetails.givenname} ${userDetails.sn}`;
  }
  return userDetails.sn;
}, [userDetails]);
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/AppLayout.tsx Outdated
Comment on lines +58 to +65
const { data: userDetails, isFetching } = useGetUserByUidQuery(loggedInUser, {
skip: !loggedInUser,
});

// Retrieve and assign user full name
const [fullName, setFullName] = React.useState<string>("");

React.useEffect(() => {
if (props.loggedInUser) {
getUserDetails(props.loggedInUser).then((response) => {
if ("data" in response) {
const first = response.data?.result.result.givenname;
const last = response.data?.result.result.sn;
// Some users (e.g., admin) don't have first name
if (!first) {
setFullName(last as string);
} else {
setFullName(first + " " + last);
}
}
});
const fullName = React.useMemo(() => {
if (!userDetails) return "";
if (userDetails?.givenname !== "") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Guard against undefined givenname when computing fullName to avoid rendering "undefined".

Because userDetails?.givenname !== "" is true when givenname is undefined, the header can show "undefined <sn>". Consider a truthiness check or explicitly handling both undefined and empty string:

const fullName = React.useMemo(() => {
  if (!userDetails) return "";
  if (userDetails.givenname) {
    return `${userDetails.givenname} ${userDetails.sn}`;
  }
  return userDetails.sn;
}, [userDetails]);

@duzda
duzda force-pushed the fix-login branch 2 times, most recently from 2b2e9a3 to 41ab00a Compare August 13, 2026 14:18

@carma12 carma12 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall nice solution. Just some details...

Comment thread src/main.css
}

.login-page-list {
list-style-type: "· ";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understand this...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the · from each string, instead it's part of the styling of the element, this more follows the common html and css schemantics. The true reason is, that I wanted to insert a link and the PF component only accepts strings...

Comment thread src/store/Global/auth-slice.ts Outdated
isUserLoggedIn: boolean;
user: string | null;
error: string | null;
loggedUser: string | null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as the user parameter you just deleted. But I understand that the new name is more descriptive... Not sure if it make sense to remove the error parameter, just in case the API call returns an error response (can't recall now the chances of that happening).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scrapped, the user now lives only in the global slice, which makes more sense, as this info was duplicated.

Comment thread src/App.tsx Outdated
// Store data in global slice (Redux)
React.useEffect(() => {
if (!isInitialBatchLoading && initialBatchResponse === undefined) {
if (initialBatchResponse === undefined) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old code had if (!isInitialBatchLoading && initialBatchResponse === undefined) which properly guarded against transient undefined states. This PR removed the !isInitialBatchLoading guard, causing a briefly flash the login page during the refetch window. Maybe this change can be be reverted?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've scraped this idea, in favor of fully relying on RTK Query, I'm curious about some checks there...

Comment thread src/App.tsx Outdated
Comment on lines +50 to +55
React.useEffect(() => {
// We need to refetch data on user change
if (!isInitialBatchLoading && loggedIn) {
refetch();
}
}, [loggedIn]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect uses isInitialBatchLoading and refetch in its body, but only declares [loggedIn] as a dependency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've scraped this idea, all of this is replace by rtk query and immediate caching, instead we refetch whenever we login.

@carma12 carma12 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall nice solution. Just some details...

Comment thread src/AppLayout.tsx Outdated
// Forcing full page to reload and redirect to login page
window.location.reload();
sessionStorage.setItem("isKerberosDisabled", "true");
dispatch(setLoggedOut());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking that there is no safeguard here (e.g. error shown) in case the logout operation fails, e.g., due to a failed response, network error, server unreachable, 500, etc. Maybe we should consider to add something here? This can be done in a different PR if needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this one has been amended? The code seems to be the same...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, true, the principle holds, please see the code:

logout().then((response) => {
      if ("data" in response && !response.data?.error) {
        sessionStorage.setItem("isKerberosDisabled", "true");
        dispatch(logoutUser());
      }
    });

In case of non-ok response (anything else but 2xx), the code throws, therefore .then will not run. We even check for errors in ok responses, not sure what else should be added here.

Comment thread src/AppLayout.tsx Outdated
// Forcing full page to reload and redirect to login page
window.location.reload();
sessionStorage.setItem("isKerberosDisabled", "true");
dispatch(setLoggedOut());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking that there is no safeguard here (e.g. error shown) in case the logout operation fails, e.g., due to a failed response, network error, server unreachable, 500, etc. Maybe we should consider to add something here? This can be done in a different PR if needed.

@duzda duzda added the needs-review This PR is waiting on a review label Aug 14, 2026

@veronnicka veronnicka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I managed to reproduce the bug, and then check that this PR really fixes it. As the code goes, Im not sure I fully understand everything but I checked all I could.

In the new PR, I stumbled upon a bug.
The steps:
1.logout from modern webui
2.transfer to old webui with the link
3.logout from old webui
4. transfer to modern webui with the link
5. logout from modern webui -> this logout fails

See the recording below:

Screencast.From.2026-08-17.13-47-50.mp4

@@ -103,7 +101,7 @@ const LoginMainPage = () => {

// Kerberos login when loading the component
React.useEffect(() => {
if (!username && isKerberosEnabled) {
if (!username && !isKerberosDisabled) {
onKrbLogin().then((response) => {
if ("error" in response) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is not and else branch to this if, Im not sure if thats a problem but it seems odd.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, if isKerberosDisabled then we don't want to perform auto-login, otherwise we would get stuck in the log in loop.

duzda added 7 commits August 18, 2026 08:33
…login after logout

Changes:
- Create a custom `LoginPage` component that accepts `loginPageContent` as a React node and renders it in the login footer
- Update `LoginMainPage` to use the custom `LoginPage` and render login instructions as a `List`, including a link back to the old WebUI
- Set an `isKerberosDisabled` flag in `localStorage` on logout to prevent automatic Kerberos re-login on the next visit
- Read and clear the flag in `LoginMainPage` to control Kerberos auto-login behavior
- Add CSS styling for the login page list bullets

Fixes: freeipa#570
Signed-off-by: David Hanina <dhanina@redhat.com>
Changes:
- Simplify `auth-slice` to track only `loggedUser` instead of separate `isUserLoggedIn`, `user`, and `error` fields
- Rename auth actions to `setLoggedUser` and `setLoggedOut`
- Remove local auth state from `App.tsx` and use the Redux `loggedUser` value directly
- Use RTK Query's `isFetching` flag for the initial batch loading state
- Remove `window.location.reload()` calls after login and logout
- Simplify `AppRoutes` by removing the `isInitialDataLoaded` prop and the `DataSpinner` fallback

Signed-off-by: David Hanina <dhanina@redhat.com>
Changes:
- Replace `loggedUser` string state with `loggedIn` boolean in auth slice
- Rename `setLoggedUser` action to `setLoggedIn`
- Retrieve logged-in user UID from global slice in `AppLayout`
- Use `useGetUserByUidQuery` instead of mutation for user details
- Refetch initial batch data when login state changes
- Move `isKerberosDisabled` flag from localStorage to sessionStorage
- Improve login error handling and validation state updates

Signed-off-by: David Hanina <dhanina@redhat.com>
Move AppLayout from a conditional wrapper in App.tsx to a parent route in
AppRoutes, rendering nested routes via Outlet. This removes the children
prop from AppLayout and simplifies the top-level App rendering.

This also fixes a bug where when logged it it incorrectly renders
sync-otp or browser-config pages.

Signed-off-by: David Hanina <dhanina@redhat.com>
- Add a `userMetadata` query to `rpcAuth` that batches the initial
  configuration commands (config_show, whoami, env, dns_is_enabled, etc.)
  and returns a typed `UserMetadata` object.
- Simplify `global-slice` to hold `UserMetadata` directly, populating it
  from the query matcher and clearing `loggedInUser` on rejection.
- Remove the dedicated `auth-slice`; derive login state from whether
  `loggedInUser` is non-empty.
- Update `App`, `AppLayout`, `AppRoutes`, `LoginMainPage`, and
  `ResetPassword` to use the new query and the `loggedInUser` global value.
- Refetch `userMetadata` on successful login and logout instead of
  toggling a boolean auth flag.

Assisted-by: Cursor <cursoragent@curosr.com>
Signed-off-by: David Hanina <dhanina@redhat.com>
Decouple the two different testing utils as importing from store
initializes API, which we want to avoid in some cases.
This can be refactored in a nicer way later on.

Signed-off-by: David Hanina <dhanina@redhat.com>
Document the authentication state model, application startup/auto-login
flow, manual login behavior, logout handling, and Kerberos re-login
prevention.

Assisted-by: Cursor <cursoragent@cursor.com>
Signed-off-by: David Hanina <dhanina@redhat.com>
@duzda

duzda commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Hi @carma12 , @veronnicka I've added a design doc to help you out comprehend how it works. Admittedly the mermaid slop is generated by AI, but it helps to get the point across and I've verified it thoroughly, the text is my own, and I tried to point out some "caveats" - more of a behaviour that was clearly wrong and fixed that.

@duzda

duzda commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@veronnicka as the bug you've mentioned I can't reproduce, from code point of view this also doesn't make sense. Maybe some data went stale during reloads

Comment thread src/AppLayout.tsx
Comment on lines +56 to +63
const fullName = React.useMemo(() => {
if (!userDetails) return "";
if (userDetails?.givenname !== "") {
return userDetails?.givenname + " " + userDetails?.sn;
}
}, [props.loggedInUser]);

return userDetails?.sn;
}, [userDetails]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check userDetails?.givenname !== "" is true when givenname is undefined (because undefined !== ""), so the header will show "undefined Smith" for users without a first name (e.g., admin). The old code had a proper truthiness check (if (!first)).

Fix suggestion:

const fullName = React.useMemo(() => {
  if (!userDetails) return "";
  if (userDetails.givenname) {
    return `${userDetails.givenname} ${userDetails.sn}`;
  }
  return userDetails.sn;
}, [userDetails]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I assumed givenname always has to be set, which is true, even administrator has givenname set, to "". Should I still handle this edge case?

Comment thread src/AppLayout.tsx Outdated
// Forcing full page to reload and redirect to login page
window.location.reload();
sessionStorage.setItem("isKerberosDisabled", "true");
dispatch(setLoggedOut());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this one has been amended? The code seems to be the same...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review This PR is waiting on a review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants